iT邦幫忙

2026 iThome 鐵人賽

0

今日目標

延續前一天完成的會員註冊與登入功能,今天開始開發智慧醫院管理平台的預約掛號後端功能。

先前已經使用 Angular 完成預約掛號、預約成功、我的預約與預約詳細等頁面,但這些頁面主要使用固定的 Demo 資料,尚未真正將預約紀錄儲存到資料庫。

因此,今天的重點是先建立正式的預約資料結構與後端 API,讓會員未來可以依照醫師的實際門診日期進行掛號,並確保不同會員的預約資料能正確區分。

今日規劃

  • 建立 Oracle 日期化診次與預約紀錄資料表。
  • 建立測試診次資料。
  • 開發 ASP.NET Core 預約掛號 API。
  • 實作預約名額控管及防止重複掛號。
  • 加入會員本人權限檢查。
  • 完成取消預約與重新掛號流程。
  • 使用 Mock 測試驗證 API。
  • 進行真實 Oracle HTTP 整合測試。

一、預約掛號資料庫設計

1. 為什麼需要建立新的診次資料表?

前面已經完成醫師基本資料與每週門診時間表,但在設計預約功能時,我發現「每週門診時間」和「實際可以預約的日期」是不同的。

例如:

  • 每週門診時間:星期一上午有門診。
  • 實際預約診次:2026/11/02 下午 14:00~17:00。
  • 預約名額:2 人。
  • 開放狀態:可以預約。

每週門診時間主要用來呈現醫師固定的看診安排,而實際預約需要有明確的日期、時間、名額及開放狀態。

因此,這次沒有直接把預約資料寫進原本的每週門診時間表,而是新增兩張資料表。

2. DOCTOR_SCHEDULES:實際門診診次

這張資料表負責保存可以掛號的診次資訊。

欄位 用途
SCHEDULE_ID 診次編號,主鍵
DOCTOR_ID 對應醫師
STARTS_AT 診次開始時間
ENDS_AT 診次結束時間
CAPACITY 預約名額
IS_BOOKABLE 是否開放預約
ROOM 診間

其中,SCHEDULE_ID 使用 Oracle Identity 自動產生,不需要由前端指定。

DOCTOR_ID 則透過 Foreign Key 對應既有的 DOCTORS 資料表,確保診次一定屬於資料庫中存在的醫師。

另外,也加入開始時間、結束時間與名額的檢查條件,避免建立不合理的診次。

3. APPOINTMENTS:會員預約紀錄

建立正式診次後,接著需要記錄哪些會員預約了哪些診次。

因此新增 APPOINTMENTS 資料表。

欄位 用途
APPOINTMENT_ID 預約編號,主鍵
PATIENT_ID 對應病人
SCHEDULE_ID 對應診次
APPOINTMENT_STATUS 預約狀態
VISIT_REASON 就診原因
REFERENCE_CODE 顯示給會員的預約識別碼
CREATED_AT 建立預約時間
CANCELLED_AT 取消時間

目前預約狀態主要分為:

  • confirmed:已預約。
  • cancelled:已取消。

這次沒有在取消預約時直接刪除紀錄,而是保留原本的預約資料,將狀態更新為 cancelled,並記錄取消時間。

這樣會員未來可以查看自己的歷史預約,也方便系統追蹤預約狀態。

4. 建立限制條件與索引

除了主鍵與外鍵,也建立了額外索引。

其中比較重要的是 UX_APPT_CONFIRMED。

它使用 Oracle 的條件式唯一索引,限制同一位病人在同一個診次中,不能同時存在兩筆有效預約。

也就是說:

  • 同一位會員不能重複預約同一診次。
  • 已取消的紀錄可以繼續保留。
  • 取消後,允許再次建立新的預約紀錄。

另外,也建立查詢診次與會員預約紀錄所需的索引。

這讓我理解到,資料庫的限制條件不只是防止錯誤資料,也能成為後端驗證之外的另一層保護。

Oracle DOCTOR_SCHEDULES、APPOINTMENTS 資料表與欄位

https://ithelp.ithome.com.tw/upload/images/20261031/20178845E0JSRrt3NG.png


二、建立 Oracle 測試診次

完成資料表後,我先建立四筆虛構診次,讓後續 API 可以使用實際日期進行測試。

診次 ID 日期 時間 名額 是否開放
1 2026/11/01 09:00~12:00 3 Y
2 2026/11/02 14:00~17:00 2 Y
3 2026/11/03 09:00~12:00 1 Y
4 2026/11/04 14:00~17:00 1 N

這次特別設定兩種測試情境。

第一種是正常開放的診次,可以測試預約建立與剩餘名額。

第二種是尚未開放預約的診次,即使資料庫中存在該診次,後端也應該拒絕建立預約。

另外,診次 3 的容量設定為 1,是為了後續測試兩位會員同時預約最後一個名額的情況。

完成新增後,我在 SQL Developer 查詢四筆診次,確認資料已正確儲存。

Oracle 四筆日期化測試診次】

https://ithelp.ithome.com.tw/upload/images/20261031/20178845gVSyBoNSL8.png

三、ASP.NET Core 預約 API 開發

資料庫準備完成後,開始建立預約掛號相關的 Web API。

這次沿用原本 ASP.NET Core 專案架構,將不同工作分配到 Controller、Service、Repository 與 DTO。

1. 預約 API 清單

HTTP API 功能
GET /api/schedules 查詢指定醫師與日期的診次
POST /api/appointments 建立預約
GET /api/appointments/me 查詢目前會員的預約
GET /api/appointments/{id} 查詢指定預約詳細
POST /api/appointments/{id}/cancel 取消預約

這次主要新增:

  • SchedulesController.cs
  • AppointmentsController.cs
  • AppointmentService.cs
  • AppointmentRepository.cs
  • AppointmentDtos.cs

Controller 負責接收 HTTP 請求與回傳結果,Service 處理預約規則,而 Repository 負責與 Oracle 資料庫溝通。

我希望讓每個部分的職責清楚,避免把名額判斷、身分檢查和 SQL 全部寫在 Controller 裡面。

2. 查詢可預約診次

第一支 API 是查詢指定醫師某一天的診次。

例如:

GET /api/schedules?doctorId=1&date=2026-11-01

後端會依照醫師及日期查詢診次,並回傳名額、剩餘名額與是否可預約等資訊。

這裡有一個重要觀念:

資料表中存在診次,不代表該診次一定可以預約。

後端還需要確認診次是否開放、是否已經開始,以及是否還有名額。

3. 建立預約

建立預約使用:

POST /api/appointments

Request 範例:

{
  "scheduleId": 3,
  "visitReason": "虛構測試就診原因"
}

會員登入後,後端會從已驗證的登入身分取得對應的病人資料。

前端只需要提供想預約的診次及就診原因,不需要自行指定 PATIENT_ID。

這樣可以避免會員修改 Request 中的病人編號,替其他人建立預約。

建立成功時回傳 HTTP 201,並提供預約詳細資訊與 Location。


四、預約名額控制與資料庫交易

這次開發過程中,我認為比較重要的問題是:

如果同一個診次只剩下一個名額,卻有兩位會員同時送出預約,系統要怎麼避免超額掛號?

如果只有在前端顯示剩餘名額,並不能真正避免這個問題。

因為不同使用者可能在幾乎相同的時間看到「剩餘 1 名」,接著同時按下預約。

1. 使用 Transaction 管理預約

這次在後端使用 Oracle Transaction 處理預約建立。

主要流程如下:

  1. 取得會員的可信身分。
  2. 開啟資料庫交易。
  3. 鎖定準備預約的診次資料列。
  4. 在取得鎖定後重新檢查診次狀態與時間。
  5. 確認目前有效預約數量。
  6. 檢查是否已額滿或重複預約。
  7. 符合條件才新增預約。
  8. 成功後 Commit;失敗則 Rollback。

Repository 使用 Oracle 的 SELECT ... FOR UPDATE WAIT 3 鎖定診次資料列,避免多個請求在名額檢查時互相干擾。

這次學到的是,剩餘名額不能只相信前一次查詢的結果,而是要在真正寫入前重新檢查。

2. 為什麼還需要唯一索引?

Transaction 主要負責控制多個請求同時操作時的資料一致性。

條件式唯一索引則負責防止同一位病人對同一個診次保留多筆有效預約。

兩者處理的問題不同,因此需要互相搭配:

  • Transaction 與診次列鎖:防止超額掛號。
  • Unique Index:防止重複有效預約。
  • Foreign Key:確保預約對應的會員與診次存在。

五、會員本人權限與取消預約

1. 只能查看自己的預約

延續 Day 26 的 Cookie Authentication,預約 API 也加入會員登入驗證。

例如會員進入:

GET /api/appointments/me

後端會依登入資訊查詢對應的 PATIENT_ID,再取得該會員自己的預約紀錄。

查詢詳細資料時,也必須同時確認預約 ID 與本人身分。

如果嘗試查看其他會員的預約,後端回傳 HTTP 404,不提供該筆資料。

因此,不能只依靠 Angular 隱藏按鈕來保護資料,真正的權限檢查仍然必須由後端執行。

2. 取消預約

取消預約使用:

POST /api/appointments/{id}/cancel

這支 API 需要會員登入與 CSRF 驗證。

取消時,後端會確認預約是否屬於本人、是否符合取消條件,並透過交易更新預約狀態。

取消成功後:

  • 將 APPOINTMENT_STATUS 更新為 cancelled。
  • 記錄 CANCELLED_AT。
  • 保留原本預約紀錄。
  • 釋出原先占用的有效預約名額。

這次也加入重複取消的處理。

如果同一筆已取消預約再次收到取消請求,系統不會重新改寫第一次的取消時間。


六、API 自動化測試

完成 API 後,我先進行 Mock HTTP 測試,再進入真實 Oracle 環境驗證。

這次使用 Mock Repository 與交易替身,測試 Controller、Service、Cookie、CSRF,以及各種預約結果。

主要涵蓋:

  • 正常建立預約。
  • 診次未開放。
  • 診次已額滿。
  • 同一會員重複預約。
  • 非本人查看或取消。
  • 取消預約與重複取消。
  • 登入及 CSRF 驗證。
  • 資料庫錯誤與交易失敗處理。

Mock 測試結果

測試 結果
Day 27 Booking Mock 286/286 通過
Day 26 Auth Mock 回歸 304/304 通過
Program 啟動回歸 27/27 通過
初期 Oracle GET-only 35/35 通過

Mock 測試能協助確認程式邏輯,但無法完全代表真實 Oracle 的交易、列鎖與並行行為,因此還需要進一步整合驗證。

預約 API 與 Mock 測試結果

https://ithelp.ithome.com.tw/upload/images/20261031/2017884569fHtM0Imq.png

七、真實 Oracle HTTP 整合測試

完成 Mock 測試後,我開始準備真實 Oracle 整合測試。

這次沒有直接使用正式會員資料,而是使用兩位既有的虛構測試會員,以及前面建立的測試診次。

1. 測試前遇到登入問題

第一次受控執行時,會員登入收到 HTTP 401,因此整個測試在建立預約之前就停止。

當時沒有新增任何預約紀錄,也沒有執行取消或 Rollback 測試。

為了避免直接重複執行造成資料不一致,我先檢查測試工具的登入流程。

過程中發現,原本使用 Console.ReadKey(true) 的密碼輸入方式,對部分編輯鍵與 Unicode 處理不夠完整。

因此,我將密碼輸入器調整為更明確的輸入與確認流程,並加入離線及 Mock 測試。

後續也建立獨立登入診斷工具,專門測試:

GET /api/auth/csrf
        ↓
POST /api/auth/login
        ↓
GET /api/auth/me

最後兩位測試會員都成功取得登入 HTTP 200,身分確認也回傳 HTTP 200。

不過,這只能證明修正後的登入流程正常,不能直接認定第一次 HTTP 401 一定是密碼輸入器造成的。

2. 使用 B 版執行真實整合測試

完成登入確認後,我重新建立獨立的受控測試計畫。

這次使用:

RunId:D27INT20261031B
Mode:Real Oracle / HTTP

測試開始前先確認:

  • Oracle Schema 正確。
  • 兩位會員與病人資料關聯完整。
  • 測試診次仍在未來。
  • 診次 3 開放且剩餘一個名額。
  • 診次 4 尚未開放。
  • 資料庫中沒有其他預約。
  • 原本失敗的測試紀錄不會被覆寫。

整個測試也限制新增預約與取消次數,避免測試意外重複執行。

3. 最後一個掛號名額測試

這次最重要的測試,是讓兩位會員同時預約只剩下一個名額的診次。

實際結果:

測試會員 HTTP 結果 說明
syy000 409 診次已額滿
syy001 201 成功建立預約

結果符合原本設計:只有一位會員成功預約,另一位收到名額不足的錯誤。

這次真實 Oracle 測試確認,兩個並行請求沒有造成超額掛號。

不過這是受控的兩請求競爭測試,尚不代表系統已通過大量使用者的效能或壓力測試。

4. 驗證取消與重新掛號

接著,我測試取消預約後是否能重新掛號。

實際流程:

  1. syy001 成功建立第一筆預約。
  2. 取消第一筆預約。
  3. 確認名額恢復。
  4. 再次建立新的預約。
  5. 取消第二筆預約。
  6. 查詢 Oracle 最終結果。

Oracle 最後保留兩筆預約歷史:

預約 ID 診次 ID 預約狀態
2 3 cancelled
3 3 cancelled

這代表取消預約不會直接刪除原本資料,而是保留歷史,並允許會員在取消後重新建立新的預約。

最終資料庫確認:

  • APPOINTMENTS:2 筆。
  • confirmed:0 筆。
  • cancelled:2 筆。
  • 診次 3 剩餘名額:1。

另外,非本人查詢及取消均得到 HTTP 404,重複預約、額滿及未開放診次也依規則回傳 HTTP 409。

Oracle 兩筆 cancelled 預約紀錄

https://ithelp.ithome.com.tw/upload/images/20261031/20178845UWSjwIFrbC.png

5. 驗證 Oracle Rollback

除了 HTTP API,這次也另外測試 Repository 的 Oracle Transaction Rollback。

測試方式是在交易內建立暫存預約並完成編號更新後,故意觸發失敗,確認資料庫是否能復原。

實際測試中,暫存預約使用 ID 1。

Rollback 完成後,重新查詢 Oracle,確認:

  • 暫存預約 ID 1 不存在。
  • 暫存測試標記不存在。
  • 沒有留下額外的有效預約。

因此,這次 Repository 層的真實 Rollback 測試通過。

需要注意的是,這是資料庫交易回復測試,並不是 HTTP 網路中斷測試。


八、真實整合測試結果

這次 Day 27 共整理了 16 項真實 Oracle 驗收項目。

驗收項目 結果
兩位會員登入 通過
未開放診次拒絕預約 通過
最後一席名額競爭 通過
成功建立預約 通過
額滿拒絕 通過
本人預約查詢 通過
非本人查詢與取消拒絕 通過
重複預約拒絕 通過
取消預約 通過
重複取消時間不變 通過
取消後釋出名額 通過
重新建立預約 通過
第二筆取消 通過
最終資料庫筆數一致 通過
Oracle Rollback 無殘留 通過

本次 16/16 項受控驗收全部通過。

這次也讓我理解,API 測試不只是確認 HTTP 有沒有回傳成功,而是要一起檢查:

  • HTTP 狀態碼是否正確。
  • 會員身分與權限是否正確。
  • Oracle 資料是否真的建立或更新。
  • 交易失敗後是否留下不完整資料。
  • 多個請求同時進入時是否超出預約名額。

九、今日遇到的問題與學習

1. 每週門診與實際預約診次不同

原本已經有醫師每週門診時間表,但正式預約必須對應到某一天的實際診次。

因此新增 DOCTOR_SCHEDULES,讓預約資料可以記錄日期、開始時間、結束時間、名額及開放狀態。

2. 前端不能直接決定剩餘名額

即使前端顯示剩餘一個名額,也可能有另一位會員同時預約。

所以真正的名額檢查需要由後端在資料庫交易中完成。

3. 預約取消不一定要刪除資料

這次將取消視為狀態變更,保留原本的預約編號與取消時間。

因此會員可以有多筆歷史紀錄,但同一個診次不能同時存在重複的有效預約。

4. Mock 通過不代表 Oracle 一定正常

一開始 API Mock 測試全部通過,但真正進行整合驗證時,仍需要另外處理登入測試工具與 Oracle 寫入流程。

也因此,我學到要區分單元/Mock 測試與真實資料庫整合測試,不能只看測試數量就認定所有功能完成。

5. 測試紀錄與防止重跑很重要

這次第一次測試在登入階段停止,因此保留原本執行紀錄,沒有直接重跑或刪除歷史。

後來使用新的 RunId 進行受控測試,讓每次執行都有對應的紀錄,也能確認實際建立、取消了哪些預約。


十、今日完成

  • 完成 Oracle DOCTOR_SCHEDULES、APPOINTMENTS 資料表。
  • 建立四筆虛構的日期化測試診次。
  • 完成五支 ASP.NET Core 預約相關 API。
  • 實作預約名額控制與 Oracle Transaction。
  • 加入條件式唯一索引,防止重複有效預約。
  • 完成會員本人預約查詢與權限限制。
  • 完成取消預約、重複取消及重新掛號流程。
  • 完成 Day 27 Booking Mock 286 項測試。
  • 完成兩位測試會員的真實登入驗證。
  • 完成 B 版真實 Oracle HTTP 預約整合測試。
  • 驗證最後一個名額的並行預約結果。
  • 驗證真實 Oracle Rollback 無殘留。
  • 完成 16/16 項受控整合驗收。

十一、下一步

Day 27 已完成預約掛號後端 API 與真實 Oracle 整合驗證。

接下來將繼續處理 Angular 與正式 API 的串接,包括會員個人資料、我的預約列表、預約詳細與取消預約畫面。

另外,正式「建立預約」的 Angular 頁面目前仍使用 Demo 流程,後續還需要將它串接至正式預約 API,才能讓會員直接從網站完成真正的掛號操作。

這次最大的收穫是,從原本只能展示的預約介面,逐步建立了能真正與 Oracle 資料庫互動的預約後端,也透過實際整合測試確認名額、權限及交易處理符合預期。


上一篇
Day 26|登入與註冊 API
下一篇
Day 28|我的預約與個人資料 API
系列文
從 Figma 到 Angular:打造智慧醫院預約掛號系統的 30 天實作之旅 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言